iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
0
自我挑戰組

寫遊戲初體驗系列 第 11

Day11 [OpenGL] Shader

  • 分享至 

  • xImage
  •  

Shader

在昨天的文章中提到過,Shader 是運行在 GPU 上的小程式。這些小程式為 Render Pipline 的某個特定部分運行。基本上來說,Shader 只是把一種輸入轉化為輸出的程式。而 Shader 之間的溝通只有透過輸入和輸出。

GLSL

GLSL(OpenGL Shading Language Language)是 OpenGL 的 Shader 語言,它長得有點像C語言,一個 Shader 通常長這樣:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()
{
  // 處理輸入並進行一些圖形操作
  ...
  // 輸出處理過的結果到輸出變數
  out_variable_name = weird_stuff_we_processed;
}

當特別談到 Vertex Shader 的時候,每個輸入輛每個輸入的變數也較做頂點屬性(Vertex Attribute),OpenGL 通常保證有 16 個的頂點屬性可以使用,在大多數情況都夠用。也可以使用GL_MAX_VERTEX_ATTRIBS來查詢具體的上限。

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

數據類型

GLSL 有C語言及其他語言大多都有的 type:int, float, double, uint, bool,另位還有兩種容器 type: 向量(Vector)跟矩陣(Matrix)

  • 向量

    • 可以有 [1, 4] 個分量
    • vecn 包含 n 個 float
    • bvecn 包含 n 個 bool
    • ivecn 包含 n 個 int
    • uvecn 包含 n 個 unsigned int
    • dvecn 包含 n 個 double
  • 分量用 .x, .y, .z, .w 來存取分量,或是rgba(顏色)跟stpq(材質)來存取。

  • 重組(Swizzling)

    • 可以用分量的組合來建立新的 vec
    vec2 someVec;
    vec4 differentVec = someVec.xyxx;
    vec3 anotherVec = differentVec.zyw;
    vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
    

輸入與輸出

我們希望每個 Shader 都有輸入以及輸出,這樣才能與其他 Shader 溝通。

GLSL 可以用 in, out 來標示一個變數是傳入還是傳出,上個 Shader 所指定的 out 會對應到下個 Shader 所指定的 in 變數,名稱要相同。

  • Vertex Shader
#version 330 core
layout (location = 0) in vec3 aPos;

out vec3 vertexColor; // to Fragment Shader

void main()
{
    gl_Position = vec4(aPos, 1.0);
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0)
}
  • Fragment Shader
#version 330 core
in vec3 vertexColor; // from Vertex Shader

out vec4 FragColor;

void main()
{
    FragColor = vertexColor;
}

我們在 Vertex Shader 宣告一個 vertexColor 的變數作為 vec4 的輸出,而在 Fragment Shader 也宣告了類似的 vertexColor。 由於名字相同,Fragment Shader 中的 vertexColor 就與 Vertex Shader 連接了。

有些人可能會想,我們能否直接從程式碼中給 Fragment Shader 發送顏色呢?

Uniform

uniform 是 CPU 向 GPU 的 Shader 發送數據的方式,但跟 Vertex Attribute 有點不同。
uniform 可以在所有的 Shader 中存取,而不用像 Vertex Attribute 是要用 in, out 傳資料,意思是 uniform 是 Global 的變數,它必須是 獨特的(Unique) 的。

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在 Code 中傳入

void main()
{
    FragColor = ourColor;sd
}

目前這個 uniform 是空的,我們需要去給他設定值。
我們首先要找到 Shader 中 uniform的索引/位置值之後,我們就可以更新他的值。
這次我不讓他固定顏色,我讓她隨時間變化顏色。

  • 傳入 uniform
    float t, blue;
    while (running)
    {
        sf::Event event;
        // SFML Event handle
        [...]
    
        // Render
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        // Pass the blue value
        t = clk.getElapsedTime().asSeconds();
        blue = (sin(t) / 2.f) + 0.5f;
        glUseProgram(program);
        glUniform4f(vertexColorLocation, 0.0f, 0.f, blue, 1.0f);
    
        // Bind Vao and Draw
        [...]
    
        // SFML display
        [...]
    

因為 OpenGL 是 C 的 library,所以不支援 Overloading。glUniform是個典型的例子。這個function後面都會接有一個特定的後綴,表示設定的 type

  • f
    • float
  • i
    • int
  • ui
    • unsigned int
  • 3g
    • 3個 float
  • fv
    • array of float

Uniform example

多個頂點屬性

如今我們希望將顏色的數據也放到 Vertex Data 中。

float vertices[] = {
    // 位置              // 顏色
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 上方
};

因為有更多的數據發送到 Shader 了,所以需要調整下 Shader。

  • Vertex Shader

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aColor;
    
    out vec3 ourColor; // 輸出到 Fragment Shader
    
    void main() {
        gl_Position = vec4(aPos, 1.0);
        ourColor = aColor;
    }
    
  • Fragment Shader

    #version 330 core
    out vec4 FragColor;
    
    in vec3 ourColor;   // 從 Vertex Shader 輸入的顏色,名稱要一樣
    
    void main() {
        FragColor = ourColor;    
    }
    

由於我們更新了頂點資料,並且更新了 VBO,所以我們需要重新配置頂點屬性的 pointer。

// Position        index,size,  type, normalize, stride,        offset 
glVertexPointerAttrib(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Color
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);

由於我們現在有了兩個頂點屬性,我們不得不重新計算步長值。為獲得數據隊列中下一個屬性值(比如位置向量的下個x分量)我們必須向右移動6個float,其中3個是位置值,另外3個是顏色值。這使我們的步長值為6乘以float的字節數(24)。
同樣,這次我們必須指定一個偏移量。對於每個頂點來說,位置頂點屬性在前,所以它的偏移量是0。顏色屬性緊隨位置數據之後,所以偏移量就是3 * sizeof(float),用字節來計算就是12字節。

參考資料

https://learnopengl.com/Getting-started/Shaders


上一篇
Day 10 [OpenGL] Hello Traingle
下一篇
Day 12 [OpenGL] Textures
系列文
寫遊戲初體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言